Hibernate Lazy&Fetch
1. 前言
本节和大家一起聊聊 Hibernate 中的 Lazy 和 Fetch 的区别,及两者适合的开发场景。通过本节课程的学习,你将了解到:
- 什么是延迟加载;
- 延迟加载的意义。
2. 又见 get() 和 load()
Session 对象提供了 2 个方法用来查询 :
- get() 方法;
- load()方法。
如果仅以结果为导向,则无法分辨两者的差异性。
两者如同双胞胎,外观虽然差异不大,但其神韵各有千秋。仔细辨别,便能发现属于各自的特征。
真相只有一个,查明真相的手段,也只有一种:让代码回答。
2.1 测试 get() 方法
Student stu=null;
try{
// 打开事务
transaction = session.beginTransaction();
//使用get()方法查询学号为1的学生
stu=(Student)session.get(Student.class, new Integer(1));
System.out.println("--------------输出学生信息------------------");
System.out.println(stu.getStuName());
transaction.commit();
} catch (Exception e) {
transaction.rollback();
} finally {
session.close();
}
System.out.println("\*\*\*\*\*\*\*\*\*\*\*关闭Session之后\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*");
System.out.println(stu.getStuName());
如上测试代码,和上一节课程的 get() 方法测试有区别:
- 调用 **get()** 方法查询编号为 1 的学生数据,但会在输出学生数据之前先输出一条提示语句,作为标识分割线;
- 关闭 Session 对象后继续使用查询出来的学生数据。
查看代码运行结果:
select
student0_.stuId as stuId1_0_0_,
student0_.stuName as stuName2_0_0_,
student0_.stuPassword as stuPassw3_0_0_,
student0_.stuPic as stuPic4_0_0_,
student0_.stuSex as stuSex5_0_0_
from
Student student0_
where
student0_.stuId=?
--------------输出学生信息------------------
Hibernate是老大
\*\*\*\*\*\*\*\*\*\*\*关闭Session之后\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
Hibernate是老大
结果能说明什么问题呢?
仔细分析输出的日志信息:
- 调用 get() 方法时,Hibernate 就构建了一条 Sql 语句。说明,调用 get() 方法时,Hibernate 就跑了一趟数据库,并拿到了开发者指定的数据;
- 关闭 Session 对象后,程序可以继续使用学生数据。说明,通过 get() 方法获得的数据已经保存到程序运行的内存中,不需要再依赖 Session。
想说明什么?不想说明什么?只是一个结论。
2.2 测试 load() 方法
把上面测试实例中的 get() 方法换成 load() 方法。
且运行实例:
Student stu=null;
try{
// 打开事务
transaction = session.beginTransaction();
//查询学号为1的学生
stu=(Student)session.load(Student.class, new Integer(1));
System.out.println("--------------输出学生信息------------------");
System.out.println(stu.getStuName());
transaction.commit();
} catch (Exception e) {
transaction.rollback();
} finally {
session.close();
}
System.out.println("\*\*\*\*\*\*\*\*\*\*\*关闭Session之后\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*");
System.out.println(stu.getStuName());
控制台上查看实例使用结果:
--------------输出学生信息------------------
Hibernate:
select
student0_.stuId as stuId1_0_0_,
student0_.stuName as stuName2_0_0_,
student0_.stuPassword as stuPassw3_0_0_,
student0_.stuPic as stuPic4_0_0_,
student0_.stuSex as stuSex5_0_0_
from
Student student0_
where
student0_.stuId=?
Hibernate是老大
\*\*\*\*\*\*\*\*\*\*\*关闭Session之后\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
Hibernate是老大
得到什么结论了吗?
现在开始寻找区别。
不仔细观察,会误判没有什么区别。
而其中有一个很明显的区别就是:
调用 load ( ) 方法时,Hiberante 并没有真正的行动,只有当执行到下面代码时:
System.out.println(stu.getStuName());
Hibernate 才从容不迫地构建 Sql 语句,往数据库跑了一趟,获得数据,再输出数据。
OK!再稍微改动一下测试代码:
//会话对象
Student stu=null;
try {
// 打开事务
transaction = session.beginTransaction();
//查询学号为1的学生
stu=(Student)session.load(Student.class, new Integer(1));
transaction.commit();
} catch (Exception e) {
transaction.rollback();
} finally {
session.close();
}
System.out.println("\*\*\*\*\*\*\*\*\*\*\*关闭Session之后\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*");
System.out.println("--------------输出学生信息------------------");
System.out.println(stu.getStuName());
输出学生信息并不是在调用 load( ) 方法之后,而是关闭 Session 对象之后,结果又会怎样?猜得出来吗?
把你心中的猜想和下面的输出结果比较一下。
没想到吧,抛异常啦,抛异常没什么大惊小怪的,异常是为了告诉你错误原因。查看异常信息,从中找出原因:
org.hibernate.LazyInitializationException: could not initialize proxy - no Session
at org.hibernate.proxy.AbstractLazyInitializer.initialize(AbstractLazyInitializer.java:164)
at org.hibernate.proxy.AbstractLazyInitializer.getImplementation(AbstractLazyInitializer.java:285)
at org.hibernate.proxy.pojo.javassist.JavassistLazyInitializer.invoke(JavassistLazyInitializer.java:185)
省略其它若干……
不要望而生畏,都是纸老虎。找出关键词:
could not initialize proxy - no Session
初看字面意思:不能初始化代理,没有 Session 对象。
什么意思?
到了好好解释这个原因的时候:
调用 load( ) 方法时,Hibernate 根本没有如你所期望一样,往数据库跑。但是又不想让你知道它没去,或是怕你担心它是否能完成这份工作。于是 Hibernate 为你提供了一个 代理对象。
这里会涉及到代理设计模式!为不影响主题学习,代理设计模式相关内容自己了解一下。
什么是代理对象?
通俗讲就是说外观和开发者所期望的对象一样,但没有实质性数据。
再通俗点,就是一个替身。有其外形,而无内涵。
再通俗讲:哦,你已经明白了。
Hibernate 这是演的哪一出?这不是摆明欺负人吗?
别误会,这是 Hibernate 的善意之举!善意从何谈起呀!别急!
只有当开发者真正需要数据时:
System.out.println(stu.getStuName());
Hibernate 才会构建 Sql 语句,往数据库跑一趟,获得真正的数据。
但是,执行 Sql 语句是一定要在 Session 的生命周期之内,如果:
session.close();
通往数据库的桥梁被拆了。Hibernate 也无能为力,只能以异常的方式告诉你:
no Session!臣妾做不到呀。
测试 get()、load() 方法的输出结果已经表明了两者的差异性:
- get() 方法言行一致,说出手呀便出手。开发者一调用,便快马加鞭,从数据库中获得数据,保存到学生对象中,只要学生对象在,数据也就在;
- load() 方法,看起来倒像是说一套,做一套的主。并不会立马行事,而是创建一个学生代理对象,提供和开发者期待的学生数据对象相同的方法接口,不影响开发者调用。只有当开发者真正需要数据时,才会说,好的,我去看一下数据库。
故而,使用 load() 时就需要特别注意,在 Hibernate 取数据库之前,千万别关闭通向数据库的桥梁:Session 对象。
Session 家里有 2 个可用于查询的兄弟:
- get() 是老实人,言行一致。
- load() 有点小调皮,有时搞点恶作剧,但心思并不坏。如果真正理解它的意图,在特定的环境下,可能会感动到你。
其实两兄弟都很有趣。
3. 延迟加载
延迟加载?不是在聊 get() 和 load() 方法吗,不是聊得好好的嘛!咋的,中场休息呀。
3.1 什么是延迟加载
什么是延迟加载?前面的测试结论已经给出了答案。
使用 Hibernate 获取数据时,有时,Hibernate 并不急着去数据库,而是等到开发者真正需要数据时才会跑一趟数据库。
load() 方法 和 get() 方法的基础区别:
- load() 支持延迟加载(Lazy);
意思是,别急,你需要时我再去拿数据。如果没有拿到数据,则会抛出异常。
- get() 方法不支持延迟加载,而是(Fetch),如果没有拿到数据,则返回 null 。
什么时候使用 get(),什么时候使用 load()。只有需求才能告诉你如何权衡,没有绝对的忠告。
3.2 延迟加载的意义
答案很简单:错峰出行,需时索取。
数据库系统的迎接能力终归是有限的。面对同时有很多数据请求时,就会造成拥堵。并不是所有的数据请求会在它的逻辑中立即使用数据。
于是,就可以使用延迟加载技术,暂缓数据请求,真正需要时,或错开数据库系统的访问高峰期后再访问。
在真实的企业级项目中,一个业务逻辑往往是借助于多个组件一起协作完成的。
Hibernate 作为数据请求框架,充当数据提供者角色,本身并不处理数据。数据的使用延迟到了数据加工组件之中。
于是,Hibernate 用不着立即造访数据库,先给数据加工组件提供一个代理对象,等数据加工组件真正需要数据时再访问数据库也不迟。
延迟加载是 Hibernate 中的性能优化技术,不要误会它是在使什么小心眼。完全是一番好意。
哲学上讲世界是平衡的,一头变轻,另一头就会变重。总能量消耗不变。
延迟加载技术提供了一种性能优化方式(变轻了),但在还没有真正获取数据之前,不能关闭 Session 对象(生命周期延长,变重了)算是平衡制约吧。
4. 小结
本节课聊到了 Hibernate 中一个很重要的概念:延迟加载,是一种性能优化技术。让开发者在真正需要数据的时候才进入到数据库。
Session 提供的 load() 方法支持延迟加载。但是,千万别以为延迟加载仅仅是 load() 方法的专利。
延迟加载是性能优化技术,Hibernate 在设计时,凡是考虑有必要使用的地方都会有延迟加载的身影。